There are a a couple of good articles on how to create a recursive ColdFusion function found on the web, and of course, the prolific ColdFusion blogger, Ben Nadel, has one, However, I had to program a recursive function for a breadcrumb widget that I introduced, and though that it would be a perfect candidate for a blog article and think that I can contribute to this conversation by putting in my own two cents in. 


What is a Recursive Function?

In programming, a recursive function handles discrete tasks that iteratively calls itself until a certain condition is met. Recursive functions are best used to handle complex tasks that need to be broken down into smaller discrete tasks. They are also quite useful when the depth of iterations is unknown at the start of the operation and/or you need to traverse up or down an object. 



Scenario

I introduced a new breadcrumb widget on this blog allowing users to traverse up the category hierarchy. The breadcrumb widget works perfectly when the user is reading a particular page, like this one, however, if the user clicks on one of the categories, such as 'Recursive Function', the breadcrumb path needs to be recreated from scratch and all that is available is the ParentCategoryId of the selected category, i.e. Recursion.

Here, our recursive function needs to query the database using the ParentCategoryId of the currently selected category. At this point, we have no idea how many levels that we need to traverse. This query should return the parent category name as well as its ParentCategoryId.

Each category record will either have a ParentCategoryId of its parent category, or 0, which indicates that the selected category is the top-level category. If the parent category record has a parent, the function will save the category information into a native ColdFusion query object and call itself again, and continue traversing up the hierarchy. Conversely, if the category's ParentCategoryId is 0, the recursive condition is resolved, the function will stop processing, and pass the ColdFusion query object back to the client.


ColdFusion Recursive Function Key Concepts

There are a few key concepts that I want to cover when developing a ColdFusion recursive function: 

  1. The function will not retain its state and data must be injected with each iteration
  2. You must have at least 2 logical branches
  3. One branch must be an exit condition when the recursive function is considered resolved
  4. If a logical branch is not resolved, it must use a cfreturn statement if it is to call the recursive function again

Although not necessary- every recursive function should have a fall-back exit condition to stop processing if things go wrong.


getParentCategoryQuery Recursive Function Explained

As you can see from the flowchart at the top of this article, this function expects a categoryId, and an optional parentCategoriesQuery. However, the parentCategoriesQuery ColdFusion query object must be sent when the function is called recursively. 

  • The function takes the categoryId to invoke the getCategory method and extracts the parentCategoryId, categoryId, category, and categoryLevel (steps 1 and 2)
  • If the parentCategoriesQuery was not passed in, a new blank ColdFusion query object is made (step 3a)
  • If the parentCategoriesQuery query was passed in, it checks to see if there are 7 or more rows in the object. If there are 7 rows, the function exits and returns the parentCategoriesQuery back to the client. This is the fallback condition in case something goes awry (steps 3b and 7)
  • The function takes the data found in getCategory function and saves the data into the parentCategoriesQuery query object (step 4).
  • If the getCategory function returns a 0 for the ParentCategoryId, it passes the parentCategoriesQuery object back to the client (step 5 and 7)
  • Otherwise, the function forwards the ParentCategoryId and parentCategoriesQuery back to itself recursively (step 6 and 1)

<cffunction name="getParentCategoryQuery" 
		hint="Finds and returns a query object of the parent categories to generate breadcrumb navigation.">
	<cfargument name="categoryId" required="true" hint="Pass in the initial cateoryId to start things off">
	<cfargument name="parentCategoriesQuery" required="false" default="" hint="this is the final list that will be returned when there are no more parentCategoryId's left">

	<!---
	Example usage:
	<cfset categoryList = getParentCategoryQuery(56,'')> 
	<cfdump var="#categoryList#">
	--->

	<!--- Get the current category (this is a HQL array) --->
	<cfset getCategory = application.blog.getCategory(categoryId=arguments.categoryId)>
	<!--- Extract the data --->
	<cfset parentCategoryId = getCategory[1]["ParentCategoryRef"]>
	<cfset categoryId = getCategory[1]["CategoryId"]>
	<cfset category = getCategory[1]["Category"]>
	<cfset categoryLevel = getCategory[1]["CategorySubLevel"]>

	<!--- Create a new three-column query, specifying the column data types ---> 
	<cfif not isQuery(parentCategoriesQuery)>
		<cfset parentCategoriesQuery = queryNew("CategoryId, ParentCategoryId, Category, CategoryLink, CategoryLevel", "integer, integer, varchar, varchar, integer")> 
		<!--- Create new rows in the query. We need as many rows as we have category levels ---> 
		<cfset queryAddRow(parentCategoriesQuery, categoryLevel)>

	</cfif><!---<cfif not isQuery(parentCategoriesQuery)>--->

	<!--- Fallback condition --->
	<cfif parentCategoriesQuery.recordcount gte 7>

		<!---There should never be more than 7 rows in this query. Send the data back as is.--->
		<cfreturn parentCategoriesQuery>

	<cfelse><!---<cfif parentCategoriesQuery.recordcount gte 7>--->
		<!--- Set the values of the cells in the query ---> 
		<cfset querySetCell(parentCategoriesQuery, "CategoryId", categoryId, categoryLevel)> 
		<cfset querySetCell(parentCategoriesQuery, "ParentCategoryId", parentCategoryId, categoryLevel)> 
		<cfset querySetCell(parentCategoriesQuery, "Category", category, categoryLevel)> 
		<cfset querySetCell(parentCategoriesQuery, "CategoryLink", application.blog.makeCategoryLink(categoryId), categoryLevel)> 
		<cfset querySetCell(parentCategoriesQuery, "CategoryLevel", categoryLevel, categoryLevel)> 

		<!--- If there is a parentCategoryId, call this function recursively and pass in the new parentCategoryId. Otherwise, return the categoryIdList. Note: we must use a cfreturn to call the function recursively, otherwise the categoryIdList will not be returned properly. --->
		<cfif parentCategoryId>
			<cfreturn getParentCategoryQuery(parentCategoryId,parentCategoriesQuery)>			
		<cfelse>
			<cfreturn parentCategoriesQuery>
		</cfif>

	</cfif><!---<cfif parentCategoriesQuery.recordcount gte 7>--->

</cffunction>

Further Reading: